﻿//-----------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation.  All Rights Reserved.
// This code is licensed under the Microsoft Public License.
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//-----------------------------------------------------------------------------
using System;
using System.Linq;
using System.Collections.Generic;
using System.IO;
using Microsoft.Cci;
using Microsoft.Cci.MetadataReader;

namespace PeToMse {
  public class SourceEmitter : BaseMetadataTraverser {

    public SourceEmitter(TextWriter writer, IMetadataHost host, PdbReader/*?*/ pdbReader) {
      this.writer = writer;
      this.host = host;
      this.pdbReader = pdbReader;
    }

    TextWriter writer;
    IMetadataHost host;
    PdbReader/*?*/ pdbReader;

    Dictionary<int, int> ids = new Dictionary<int, int>();
    int GetID(object o) { // TODO: get stable hash
        int id;
        if (!ids.TryGetValue(o.GetHashCode(), out id))
            ids[o.GetHashCode()] = id = ids.Count;
        return id;
    }

    IModule module;
    public override void Visit(IModule module)
    {
        this.module = module;
        this.WriteDef("Namespace", module);
        this.Write("name", module.Name.Value);
        this.CloseDef();
        base.Visit(module);

        foreach (var subclass in subclasses)
        {
            foreach (var c in subclass.Value)
            {
                this.WriteDef("Inheritance", new object());
                this.WriteRef("subclass", c);
                this.WriteRef("superclass", subclass.Key);
                this.CloseDef();
            }
        }
    }

    public override void  Visit(IAssemblyReference assemblyReference)
    {
        this.WriteDef("Namespace", assemblyReference);
        this.Write("name", assemblyReference.AssemblyIdentity.Name.Value);
        this.CloseDef();        
 	    base.Visit(assemblyReference);
    }

    public override void Visit(ITypeReference typeReference)
    {
        GetReferenceID(typeReference);
        base.Visit(typeReference);
    }

    HashSet<uint> visitedReferences = new HashSet<uint>();
    private void GetReferenceID(ITypeReference typeReference)
    {
        if (this.visitedReferences.Add(typeReference.InternedKey)) // better hashing?
        {
            this.WriteDef("Class",typeReference);
            this.WriteRef("container", TypeHelper.GetDefiningUnitReference(typeReference)); // TODO: module containing typeReference
            this.Write("name", TypeHelper.GetTypeName(typeReference));
            this.Write("isStub", true);
            this.CloseDef();
        }
    }

    HashSet<IPrimarySourceLocation> visitedDocuments = new HashSet<IPrimarySourceLocation>();
    private void GetSourceLocation(IPrimarySourceLocation doc)
    {
        if (this.visitedDocuments.Add(doc))
        {
            this.WriteDef("FileAnchor", doc);
            this.Write("fileName", doc.PrimarySourceDocument.Location);
            this.Write("startLine", doc.StartLine);
            this.Write("endLine", doc.EndLine);
            this.CloseDef();
        }
    }

    Dictionary<ITypeReference, List<ITypeDefinition>> subclasses = new Dictionary<ITypeReference, List<ITypeDefinition>>();
    public override void Visit(ITypeDefinition typeDefinition)
    {
        this.WriteDef("Class", typeDefinition);
        this.Write("name", TypeHelper.GetTypeName(typeDefinition));
        this.WriteRef("container", this.module);
        this.CloseDef();

        foreach (var baseclass in typeDefinition.BaseClasses)
        {
            List<ITypeDefinition> sc;
            if (!subclasses.TryGetValue(baseclass, out sc))
                subclasses[baseclass] = sc = new List<ITypeDefinition>();
            sc.Add(typeDefinition);
        }

        base.Visit(typeDefinition);
    }

    public override void Visit(IFieldDefinition fieldDefinition)
    {
        base.Visit(fieldDefinition);

        this.WriteDef("Attribute", fieldDefinition);
        this.Write("name", fieldDefinition.Name.Value);
        this.WriteRef("parentType", fieldDefinition.ContainingType);
        this.WriteRef("declaringType", fieldDefinition.Type);
        this.Write("hasClassScope", !fieldDefinition.IsStatic);
        this.Write("isPublic", fieldDefinition.Visibility == TypeMemberVisibility.Public);
        this.CloseDef();
    }

    public override void Visit(IMethodDefinition method)
    {
        string fileName;
        int startLine;
        int endLine;
        List<IMethodReference> calls;
        int cc;
        ProcessInstructions(method, out fileName, out startLine, out endLine, out calls, out cc);

        object sourceAnchor = WriteSourceAnchor(fileName, startLine, endLine);

        this.WriteDef("Method", method);
        if (sourceAnchor != null)
            this.WriteRef("sourceAnchor", sourceAnchor);
        this.WriteRef("parentType", method.ContainingTypeDefinition);
        this.Write("name", method.Name.Value);
        this.Write("signature", MemberHelper.GetMethodSignature(method, NameFormattingOptions.Signature));
        this.Write("isPublic", method.Visibility == TypeMemberVisibility.Public);
        this.Write("isVirtual", method.IsVirtual);
        this.Write("isAbstract", method.IsAbstract);
        if (startLine <= endLine)
            this.Write("LOC", endLine - startLine + 1);
        this.Write("CC", cc);
        this.CloseDef();

        foreach (var call in calls)
        {
            // TODO: resolve virtual method overloads
            this.WriteDef("Invocation", new object());
            this.WriteRef("sender", method);
            this.WriteRef("candidates", call);
            this.Write("signature", MemberHelper.GetMethodSignature(call, NameFormattingOptions.Signature));
            this.CloseDef();
        }

        base.Visit(method);
    }

    private object WriteSourceAnchor(string fileName, int startLine, int endLine)
    {
        object sourceAnchor = null;
        if (startLine <= endLine)
        {
            this.WriteDef("FileAnchor", sourceAnchor = new object());
            this.Write("fileName", fileName);
            this.Write("startLine", startLine + 1);
            this.Write("endLine", endLine + 1);
            this.CloseDef();
        }
        return sourceAnchor;
    }

    private void ProcessInstructions(IMethodDefinition method, out string fileName, out int startLine, out int endLine, out List<IMethodReference> calls, out int cc)
    {
        fileName = null;
        startLine = int.MaxValue;
        endLine = int.MinValue;

        calls = new List<IMethodReference>();
        cc = 2; // number of decision points - number of exit points + 2
        foreach (var operation in method.Body.Operations)
        {
            if (this.pdbReader != null)
                foreach (var ploc in this.pdbReader.GetPrimarySourceLocationsFor(operation.Location))
                {
                    if (ploc.Length == 0) continue;
                    if (fileName == null)
                        fileName = ploc.PrimarySourceDocument.Location;
                    else if (fileName != ploc.PrimarySourceDocument.Location)
                        continue; // skip different file
                    startLine = Math.Min(startLine, ploc.StartLine);
                    endLine = Math.Max(endLine, ploc.EndLine);
                }

            switch (operation.OperationCode)
            {
                case OperationCode.Call:
                case OperationCode.Calli:
                case OperationCode.Callvirt:
                    var callee = operation.Value as IMethodReference;
                    if (callee != null)
                        calls.Add(callee);
                    break;
                case OperationCode.Beq:
                case OperationCode.Beq_S:
                case OperationCode.Bge:
                case OperationCode.Bge_S:
                case OperationCode.Bge_Un:
                case OperationCode.Bge_Un_S:
                case OperationCode.Bgt:
                case OperationCode.Bgt_S:
                case OperationCode.Bgt_Un:
                case OperationCode.Bgt_Un_S:
                case OperationCode.Ble:
                case OperationCode.Ble_S:
                case OperationCode.Ble_Un:
                case OperationCode.Ble_Un_S:
                case OperationCode.Blt:
                case OperationCode.Blt_S:
                case OperationCode.Blt_Un:
                case OperationCode.Blt_Un_S:
                case OperationCode.Bne_Un:
                case OperationCode.Bne_Un_S:
                case OperationCode.Brfalse:
                case OperationCode.Brfalse_S:
                case OperationCode.Brtrue:
                case OperationCode.Brtrue_S:
                    cc++;
                    break;
                case OperationCode.Ret:
                    cc--;
                    break;
            }
        }
    }

    private void WriteDef(string name, object id)
    {
        this.writer.WriteLine("(FAMIX.{0} (id: {1})", name, this.GetID(id));
    }

    private void CloseDef()
    {
        this.writer.WriteLine(")");
    }

    private void WriteRef(string name, object reference)
    {
        if (reference != null)
            this.writer.WriteLine("    ({0} (ref: {1}))", name, this.GetID(reference));
    }

    private void Write(string name, int value)
    {
        this.writer.WriteLine("    ({0} {1})",name, value);
    }


    private void Write(string name, string value)
    { 
        if (!string.IsNullOrEmpty(value))
            this.writer.WriteLine("    ({0} '{1}')", name, value.Replace("'", "\'"));
    }

    private void Write(string name, bool value)
    {
        if (value)
            this.writer.WriteLine("    ({0} true)", name);
    }
  }
}